package hu.dobrosi.util.io.gps;

import hu.dobrosi.dinkycam.j2me.DinkycamApp;
import hu.dobrosi.util.StringUtil;
import hu.dobrosi.util.io.BufferedInputStreamReader;
import hu.dobrosi.util.io.StreamConnectionConnector;

import java.io.IOException;
import java.util.Calendar;
import java.util.TimeZone;

/**
 * @author andris
 * 
 */
public class GPSReader {

	/**
	 * álló helyzet esetén a sebesség ezen az időszakon belül alacsonyabb a
	 * <code>MIN_SPEED</code> értéknél
	 */
	public static double MIN_DELTA_FIX = 5 * 60 * 1000;

	/**
	 * álló helyzet megállapításához a legnagyobb megengedett pozició változás a
	 * 3 koordinátát egyszerre figyelve
	 */
	public static double MIN_DELTA_POS = 0.0005;

	/**
	 * álló helyzet megállapításához a legkisebb megengedett sebesség változás
	 */
	public static double MIN_SPEED = 5.0D;

	private double[] location = new double[] { 0, 0, 0 };

	private Calendar calendar;

	private double direction = -1.0d;

	private double velocity = -1.0d;

	private boolean hasSignal;

	private String dump;

	private long lastFixInMillis = -1;

	private StreamConnectionConnector connector;

	private String url;

	private double lastLatitude;

	private double lastLongitude;

	private double lastAltitude;

	private LocationHandler loc;

	private Exception lastException;

	public GPSReader(String url) {
		this.url = url;
		calendar = Calendar.getInstance();
		calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
	}

	/**
	 * lezárja a kapcsolatot a GPS vevővel.
	 * 
	 * @throws IOException
	 */
	public void close() throws IOException {
		if (connector != null) {
			connector.close();
		}
	}

	/**
	 * Kapcsolódik a GPS vevőhöz és a <code>lineNumber</code> számú sort olvas
	 * be a vevőről. A beolvasott sorokat egyben fel is dolgozza.
	 * 
	 * @param lineNumber
	 * @throws IOException
	 */
	public synchronized void readDevice(int lineNumber) throws IOException {
		if (!DinkycamApp.isEmulator()) {
			if (url != null) {
				hasSignal = false;
				lastException = null;
				if (connector == null) {
					connector = new StreamConnectionConnector();
				}
				try {
					connector.open(url);
					BufferedInputStreamReader reader = new BufferedInputStreamReader(
							connector.getInputStream(), 30000);
					int powerSafeModeCounter = 0;
					while (lineNumber > powerSafeModeCounter++) {
						try {
							processInput(reader);
						} catch (Exception e) {
							lastException = e;
							StringUtil.logger.clear();
							StringUtil.logger.print(e);
						}
					}
					reader.close();
					connector.close();
				} catch (IOException e) {
					lastException = e;
					StringUtil.logger.clear();
					StringUtil.logger.print(e);
					throw e;
				}
			} else { // JSR 179
				if (loc == null) {
					loc = new LocationHandler();
				}
				loc.read(lineNumber);
				location = loc.getLocation();
				calendar = loc.getCalendar();
				velocity = loc.getVelocity();
			}
		} else {
			location = new double[] { 47.0 + hu.dobrosi.util.Math.random(),
					19.0 + hu.dobrosi.util.Math.random(),
					100.0 + hu.dobrosi.util.Math.random() };
			hasSignal = true;
			calendar = Calendar.getInstance();
		}
	}

	public String toString() {
		return "lat=" + getLatitude() + "&lon=" + getLongitude() + "&alt="
				+ getAltitude() + "&fix=" + getFix().getTime() + "&v="
				+ velocity + "&d=" + direction + " " + (isMoving() ? " " : "N")
				+ (calendar.getTime().getTime() - lastFixInMillis) / 1000;
	}

	private boolean processInput(BufferedInputStreamReader reader)
			throws IOException {
		dump = reader.readLine();
		if (dump != null) {
			Object[] sp = StringUtil.splitString(dump, ",");
			String command = sp[0].toString().toUpperCase();
			int plus = -1;
			String timeString = null;
			String dateString = null;
			if (command.equals("$GPGLL")) {
				plus = 0;
				timeString = sp[5].toString();
				hasSignal = true;
			} else if (command.equals("$GPGGA")) {
				plus = 1;
				timeString = sp[1].toString();
				if (!StringUtil.isEmpty(sp[9].toString())) {
					try {
						location[2] = Double.parseDouble(sp[9].toString());
					} catch (Exception exc) { // EXC
					}
				}
				hasSignal = true;
			} else if (command.equals("$GPRMC")) {
				if (!"A".equals(sp[2])) {
					hasSignal = false;
				} else {
					plus = 2;
					try {
						timeString = sp[1].toString();
						dateString = sp[9].toString();
						velocity = Double.parseDouble(sp[7].toString());
						direction = Double.parseDouble(sp[8].toString());
					} catch (Exception exc) { // EXC
					}
					hasSignal = true;
				}
			}
			if (plus >= 0) {
				try {
					location[0] = toCoordinate(sp[1 + plus].toString(),
							sp[2 + plus].toString());
					location[1] = toCoordinate(sp[3 + plus].toString(),
							sp[4 + plus].toString());
				} catch (Exception exc) { // EXC
				}
			}
			if (timeString != null) {
				try {
					setCalendar(timeString, dateString);
				} catch (Exception exc) { // EXC
				}
			}
		}

		return hasSignal;
	}

	public void setCalendar(String fixTimeString, String fixDateString) {
		if (fixTimeString != null && fixTimeString.length() >= 6) {
			calendar.set(Calendar.HOUR_OF_DAY,
					Integer.parseInt((fixTimeString.substring(0, 2))));
			calendar.set(Calendar.MINUTE,
					Integer.parseInt(fixTimeString.substring(2, 4)));
			calendar.set(Calendar.SECOND,
					Integer.parseInt(fixTimeString.substring(4, 6)));
			calendar.set(Calendar.MILLISECOND, 0);
		}
		if (fixDateString != null && fixDateString.length() >= 6) {
			calendar.set(Calendar.DAY_OF_MONTH,
					Integer.parseInt(fixDateString.substring(0, 2)));
			calendar.set(Calendar.MONTH,
					Integer.parseInt(fixDateString.substring(2, 4)) - 1);
			int century = calendar.get(Calendar.YEAR) / 100;
			int year = Integer.parseInt(fixDateString.substring(4, 6));
			if (calendar.get(Calendar.YEAR) % 100 == 99 && year == 0) {
				century++;
			}
			calendar.set(Calendar.YEAR, century * 100 + year);
		}
	}

	/**
	 * Visszaadja, hogy az eszköz mozgásban van-e. Figyelembe veszi a sebesség
	 * és pozició változást. lsd. <code>MIN_SPEED</code>,
	 * <code>MIN_DELTA_FIX</code>, <code>MIN_DELTA_POS</code>.
	 * 
	 * @return ha mozgásban van, akkor <code>true</code>
	 */
	public boolean isMoving() {
		if (hasSignal() && checkSpeedLimit() && checkPositionLimit()) {
			lastFixInMillis = System.currentTimeMillis();
		}
		lastLatitude = getLatitude();
		lastLongitude = getLongitude();
		lastAltitude = getAltitude();
		return System.currentTimeMillis() - lastFixInMillis < MIN_DELTA_FIX;
	}

	private boolean checkPositionLimit() {
		return Math.abs(lastLatitude - getLatitude()) > MIN_DELTA_POS
				|| Math.abs(lastLongitude - getLongitude()) > MIN_DELTA_POS;
	}

	private boolean checkSpeedLimit() {
		return velocity < 0 || velocity > MIN_SPEED || lastFixInMillis < 0;
	}

	/**
	 * Visszatér a pozició adatok tömbjével.
	 * 
	 * @return
	 */
	public double[] getLocation() {
		return location;
	}

	/**
	 * viszatér a szélességi adattal
	 * 
	 * @return
	 */
	public double getLatitude() {
		return hasSignal() && location[0] != 0.0 ? location[0] : lastLatitude;
	}

	/**
	 * Visszatér a hosszúsági adattal
	 * 
	 * @return
	 */
	public double getLongitude() {
		return hasSignal() && location[1] != 0.0 ? location[1] : lastLongitude;
	}

	/**
	 * Visszatér a magassági adattal
	 * 
	 * @return
	 */
	public double getAltitude() {
		return hasSignal() && location[2] != 0.0 ? location[2] : lastAltitude;
	}

	/**
	 * Visszatér a mozgás irány adattal
	 * 
	 * @return
	 */
	public double getDirection() {
		return direction;
	}

	/**
	 * Visszatér a sebesség adattal
	 * 
	 * @return
	 */
	public double getVelocity() {
		return velocity;
	}

	/**
	 * Visszatér a pontos időpontot tartalmazó <code>Calendar</code>
	 * objektummal, amely időzónája UTC
	 * 
	 * @return
	 */
	public Calendar getFix() {
		return calendar;
	}

	/**
	 * Értelmezhető jel érkezését vizsgálhatjuk.
	 * 
	 * @return ha van értelmezhető jel, akkor értéke <code>true</code>
	 */
	public boolean hasSignal() {
		return hasSignal;
	}

	private double toCoordinate(String s, String dir) {
		double deg = 0;
		int i = s.indexOf('.');
		if (i >= 0) {
			int divider = 1;
			int size = s.length();
			for (int d = size - i - 1; d > 0; d--)
				divider *= 10;
			deg = (double) Integer.parseInt((s.substring(0, i - 2)))
					+ ((double) Integer.parseInt(s.substring(i - 2, i)) + (double) Integer
							.parseInt(s.substring(i + 1, size)) / divider) / 60;
			if (dir.startsWith("S") || dir.startsWith("W"))
				deg = -deg;
		}
		return deg;
	}

	/**
	 * @return the lastException
	 */
	public Exception getLastException() {
		return lastException;
	}

	/**
	 * @param lastException
	 *            the lastException to set
	 */
	public void setLastException(Exception lastException) {
		this.lastException = lastException;
	}

}
